弱引用table

  Lua采用了自动内存管理。一个程序只需创建对象,而无须删除对象。通过使用垃圾收集机制,Lua会自动地删除那些已成为垃圾的对象。这减轻了程序员在内存管理方面的负担,更重要的是将程序员从许多内存相关的bug(例如无效指针、内存泄漏)中解放出来。

  Lua的垃圾收集器与一些其他的收集器有所不同,它没有环形引用的问题。当用到环形数据结构时,无须作出任何特殊的处理,它们也可以像其他数据一样被正常回收。不过,有时即使是再聪明的收集器也需要帮助。垃圾收集器无法解决所有内存管理的问题。

  垃圾收集器只能回收那些它认为是垃圾的东西,它不会回收那些用户认为是垃圾的东西。一个典型的例子就是栈,栈通常由一个数组和一个表示顶部的索引来实现。这个数组的有效部分总是向顶部扩展的,但Lua却不知道。如果弹出一个元素时只是简单地递减顶部索引,那么这个仍留在数组中的对象对于Lua来说就不是垃圾。同理,对于那些存储在全局变量中的对象,即使程序不会再用到它们,但对于Lua来说就不是垃圾。在这两种情况中,都需要由用户来将这些对象变量赋值为nil。这样才能使它们得以释放。

  不过,简单地清楚引用可能还不够。有些情况需要程序和收集器之间进行更多的协作。例如,如果要将一些对象放在一个数组中,这看似简单,好像只需把每个对象插入数组即可。但是,当一个对象处于数组中时,它就无法被回收。这是因为即使当前没有其他地方在使用它,但数组仍引用着它。除非用户告诉Lua这项引用不应该阻碍此对象的回收,否则,Lua是无从得知这个事实的。

  弱引用tableweak table)就是这样一种机制,用户能用它来告诉Lua一个引用不应该阻碍一个对象的回收。所谓“弱引用(weak reference)”就是一种会被垃圾收集器忽视的对象引用。如果一个对象的所有引用都是弱引用,那么Lua就可以回收这个对象了,并且还可以以某种形式来删除这些弱引用本身。Lua用“弱引用table”来实现“弱引用”,一个弱引用table就是一个具有弱引用条目的table。如果一个对象只被一个弱引用table所持有,那么最终Lua是会回收这个对象的。

  table中有keyvalue,这两者都可以包含任意类型的对象。通常,垃圾收集器不会回收一个可访问table中作为keyvalue的对象。也就是说,这些keyvalue都是强引用(strong reference),它们会阻止对其所引用对象的回收。在一个弱引用table中,keyvalue是可以回收的。有3种弱引用table:具有弱引用keytable、具有弱引用valuetable、同时具有两种弱引用table。不论是哪种类型的弱引用table,只要有一个keyvalue被回收了,那么它们所在的整个条目都会从table中删除。

  一个table的弱引用类型是通过其元表中的__mode字段来决定的。这个字段的值应为一个字符串,如果这个字符串中包含字母‘k’,那么这个table的key是弱引用的;如果这个字符串中包含字母‘v’,那么这个tablevalue是弱引用的。下面这个示例虽然是人为制造的,但演示了弱引用table的一些基本行为:

    a = {}
    b = {__mode = 'k'}
    setmetatable(a, b)    -- 现在'a'的key就是弱引用
    key = {}              -- 创建第一个key
    a[key] = 1
    key = {}              -- 创建第二个key
    a[key] = 2
    collectgarbage()      -- 强制进行一次垃圾收集
    for k, v in pairs(a) do print(v) end
    --> 2

  在本例中,第二句赋值key = {}会覆盖第一个key。当收集器运行时,由于没有其他地方在引用第一个key,因此第一个key就被回收了,并且table中的相应条目也被删除了。至于第二个key,变量key仍引用着它,因此它没有被回收。

  注意,Lua只会回收弱引用table中的对象。而像数字和布尔这样的“值”是不可回收的。例如,对于一个插入table的数字key,收集器是永远不会删除它的。当然,如果一个数字key所对应的value被回收了,那么整个条目都会从这个弱引用table中删除。

  字符串在此则显得有些特殊。虽然从实现的角度看,字符串是可回收的。但在有些方面,字符串却与其他可回收的对象不同。其他对象,例如table和函数都是显式创建的。又如,当Lua对表达式{}求值时,它就会创建一个新的table。同样地,求值function()...end时就会创建一个新函数。然而,当Lua对"a".."b"求值时,它会创建一个新字符串吗?如果当前系统中已有了一个字符串"ab",它会复用吗?还是创建一个新的字符串?编译器会在运行程序前先创建这个字符串吗?这些都无关紧要,它们都是实现的细节。从程序员的角度看,字符串就是值,而非对象。因此,字符串就像数字和布尔一样,不会从弱引用table中删除。

  

备忘录(memoize)函数

  一项通用的编程技术是“用空间换时间”。例如有一种做法就可以提高一些函数的运行速度,记录下函数计算的结果,然后当使用同样的参数再次调用该函数时,便可以复用之前的结果了。

  假设有一个普通的服务器,在它收到的请求中包含Lua代码。每当服务器收到一个请求,它就要对代码字符串调用loadstring,然后再调用编译好的函数。不过,loadstring是一个昂贵的函数,而有些发给服务器的命令具有很高的频率,例如“closeconnection()”。与其每次收到一条常见命令就调用loadstring,还不如让服务器用一个辅助的table记录下所有调用loadstring的结果。因此,在每次调用loadstring前,服务器先检查table中是否已记录了代码字符串编译后的结果。如果没有,才调用loadstring,并将结果存储到table中。可以将这个行为写成一个新函数:

    local results = {}
    funcition mem_loadstring(s)
        local res = results[s]
        if res == nil then        -- 是否已记录过?
            res = assert(loadstring(s))        -- 计算新结果
            results[s] = res        -- 存下以备之后复用
        end
        return res
    end

  这项优化节省的时间非常可观。但,它也可能导致不易察觉的开销。虽然有些命令会重复出现,但还有许多命令只发生一次。例如,table results会逐渐地积累服务器收到的所有命令及其编译结果。经过一定的时间后,这种累积会耗费服务器的内存。弱引用的table正好可以解决这个问题,如果results table具有弱引用的value,那么每次垃圾收集都会删除所有在执行时未使用的编译结果。

    local results = {}
    setmetatable(results, {__mode = 'v'})        -- 使用value称为弱引用
    funcition mem_loadstring(s)
        <如前>

  实际上,由于key总是字符串,则可以使这个table编程完全弱引用。若这么做:

    setmetatable(results, {__mode = 'kv'})

  则最终效果完全一样。

  “备忘录”技术还可以用于确保某类对象的唯一性。假设一个系统用table来表示颜色,其中3个字段redgreenblue都具有相同的取值范围。最简单的颜色生成函数是:

    function createRGB(r, g, b)
        return {red=r, green=g, blue=b}
    end

  通过备忘录技术,可以复用具有相同颜色的table。备忘录tablekey可以根据颜色分量来生成,本例中是将颜色分量以分隔符连接起来:

    local results = {}
    setmetatable(result, {__mode='v'})        -- 使用value成为弱引用
    function createRGB(r, g, b)
        local key = r .. "-" .. g .. "-" .. b
        local color = results[key]
        if color == nil then
            color = {red=r, green=g, blue=b}
            results[key] = color
        end
        return color
    end

  这种实现可以使用户通过原始的相等性操作符比较两种颜色。若两种同时存在的颜色相等,那么它们必定是由同一个table表示的。不过,相同的颜色也可能在不同时间由不同的table表示,这是因为期间执行过垃圾收集,清除了results table。只要一种颜色正在使用,就不会被清除出results。因此,只要一个颜色未被清除,它就可与新颜色进行比较,它的表示也可作为后续调用来复用。

  

对象属性

  关于弱引用table,还有一项重要的应用是将属性与对象关联起来。有很多情况需要把有些属性绑定到某个对象,例如函数与其名称、table的默认值及数组的大小等。

  当对象是一个table时,可以通过适当的key将属性存储在这个table中。正如先前所看到的,创建唯一性key的最简单办法是创建一个新对象(通常是一个table)。不过,若对象不是一个table,它就无法保存属性了。另外,即使是table,有时也不想将属性存储在原table中。例如,想保持属性的私有性,或者不想让属性扰乱table的遍历就需要用其他办法来关联属性与对象了。显然,使用外部table来关联它们是一种理想的做法。可以将对象作为key,对象的属性作为value。这个外部table可以保存任意对象的属性,Lua也允许将任何对象作为tablekey。另外,存储在外部对象中的属性不会干扰其他对象,只要table本身是私有的,这些属性也会是私有的。

  然而,这个看似完美的做法却有一个重大缺陷。当用户将一个对象作为外部tablekey时,就是引用了它。Lua是无法回收一个作为table key的对象。如果用这个外部table来关联函数和函数名,那么这些函数就永远无法回收。用户可以使用弱引用table来解决这个问题。而本例需要的是弱引用key。当一个弱引用key没有其他引用时,Lua就可以回收它。注意,这个table不能使用弱引用value,否则“存留的”对象的属性就有可能被回收。

  

回顾table的默认值

  前面章节讨论了如何实现具有非nil默认值的table。库定义的元方法一节中注明了还有两种技术需要弱引用table的支持。这里将介绍两种用于默认值的技术,它们其实是上述备忘录和对象属性的特殊应用。

  第一种做法是使用一个弱引用table,通过它将每个table与其默认值关联起来:

    local defaults = {}
    setmetatable(defaults, {__mode='k'})
    local mt = {__index=function(t) return defaults[t] end}
    function setDefault(t, d)
        defaults[t] = d
        setmetatable(t, mt)
    end

  如果defaults没有弱引用key,它就会使所有具有默认值的table持久存在下去。

  第二种做法是对每种不同的默认值使用不同的元表。不过,只要有重复的默认值,就复用同样的元表。这是备忘录的典型应用:

    local metas = {}
    setmetatable(metas, {__mode = 'v'})
    function setDefault(t, d)
        local mt = metas[d]
        if mt == nil then
            mt = {__index = function() return d end}
            metas[d] = mt             -- 备忘录
        end
        setmetatable(t, mt)
    end

  这里用到了弱引用value,这样当metas中的元表在不使用时就可以被回收了。

  这两种默认值的实现,哪种更好呢?一般而言,它们具有类似的复杂度和性能表现。第一种做法需要为每个table的默认值(defaults中的一个条目)使用内存。而第二种做法则需要为每种不同的默认值使用一组内存(一个新table,一个新closuremetas中的一个条目)。因此,如果程序中有上千个table和一些默认值,第二种做法则是首选。但如果只有很少的table共享几个公用的默认值,那么就应该选择第一种做法。

🔚

results matching ""

    No results matching ""